//-------------------------------------------------------------------------------------------------
// Copyright (c) Bradford W. Mott and Flare Contributors
// North Carolina State University, Department of Computer Science
// The IntelliMedia Group
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
// OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
// SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
// OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//-------------------------------------------------------------------------------------------------

using UnityEngine;
using ICloneable = System.ICloneable;
using System.Collections.Generic;
using Flare.Events;
using Flare.Geom;

namespace Flare.Display
{
    /// <summary>
    /// The DisplayObject class is an abstract base class for objects that can be added to
    /// the display list. Display objects can be added to a DisplayObjectContainer, which
    /// allows hierarchical display lists to be created.
    /// </summary>
    public abstract class DisplayObject : EventDispatcher, ICloneable
    {
        /// <summary>
        /// Alpha transparency of the display object.
        /// </summary>
        public float alpha
        {
            get { return transform.colorTransform.alphaMultiplier; }
            set { transform.colorTransform.alphaMultiplier = value; }
        }

        /// <summary>
        /// Specifies which blend mode to use when rendering the display object.
        /// </summary>
        public string blendMode { get; set; }

        /// <summary>
        /// LoaderInfo associated with this object.
        /// </summary>
        public LoaderInfo loaderInfo { get; 
            /* \cond */ internal set; /* \endcond */ }

        /// <summary>
        /// Associates a mask with the display object that will mask the display object
        /// when displayed (only the parts of the display object that overlay the mask
        /// will be visible). The mask object should be added to the display list so
        /// that it is properly updated; however, it will not be visible itself. To remove
        /// the mask from a display object set mask to null.
        /// 
        /// Unlike AS3, a single mask object can be used to mask multiple display objects.
        /// </summary>
        public DisplayObject mask
        {
            get
            {
                return this.m_mask;
            }

            set 
            {
                if (this.m_mask != null)
                {
                    this.m_mask.m_maskingObjectCount -= 1;
                }

                if (value != null)
                {
                    value.m_maskingObjectCount += 1;
                }

                this.m_mask = value; 
            }
        }

        /// <summary>
        /// Returns true if the display object is being used as a mask.
        /// </summary>
        public bool isMask
        {
            get { return this.m_maskingObjectCount > 0; }
        }
        
        private DisplayObject m_mask = null;
        private int m_maskingObjectCount = 0;

        /// <summary>
        /// Name of the display object.
        /// </summary>
        public string name { get; set; }
        
        /// <summary>
        /// DisplayObjectContainer object that contains this display object.
        /// </summary>
        public DisplayObjectContainer parent { get; set; }
        
        /// <summary>
        /// Rotation of the display object in degrees (1 to 180 represents a clockwise
        /// rotation while -1 to -180 represents a counterclockwise rotation).
        /// </summary>
        public float rotation
        {
            get
            {
                Matrix m = this.transform.matrix;
                return (float)(global::System.Math.Atan2(m.b, m.a) *
                    (180.0 / global::System.Math.PI));
            }
            
            set
            {
                Matrix m = this.transform.matrix;
                double current = global::System.Math.Atan2(m.b, m.a);
                double angle = (value * (global::System.Math.PI / 180.0)) - current;
                double sin = global::System.Math.Sin(angle);
                double cos = global::System.Math.Cos(angle);

                float a = (float)(m.a * cos - m.b * sin);
                float b = (float)(m.a * sin + m.b * cos);
                float c = (float)(m.c * cos - m.d * sin);
                float d = (float)(m.c * sin + m.d * cos);

                m.a = a;
                m.b = b;
                m.c = c;
                m.d = d;
            }
        }

        /// <summary>
        /// Horizontal scale of the display object (1.0 is 100% scale). Note that negative
        /// scales along both axes results in only positive scales with an appropriate
        /// rotation; while a negative scale along a single axis results in a negative scale
        /// along the y axis. Unlike AS3, scaling to a non-zero value from a scale of zero
        /// results in the b component of the transform matrix being set to zero.
        /// </summary>
        public float scaleX
        {
            get
            {
                Matrix m = this.transform.matrix;
                return (float)(global::System.Math.Sqrt(m.a * m.a + m.b * m.b));
            }
            
            set
            {
                Matrix m = this.transform.matrix;
                float sx = (float)(global::System.Math.Sqrt(m.a * m.a + m.b * m.b));

                if (sx != 0.0f)
                {
                    m.a = (m.a / sx) * value;
                    m.b = (m.b / sx) * value;
                }
                else
                {
                    m.a = value;
                    m.b = 0.0f;
                }
            }
        }
        
        /// <summary>
        /// Vertical scale of the display object (1.0 is 100% scale). Note that negative
        /// scales along both axes results in only positive scales with an appropriate
        /// rotation; while a negative scale along a single axis results in a negative scale
        /// along the y axis. Unlike AS3, scaling to a non-zero value from a scale of zero
        /// results in the c component of the transform matrix being set to zero.
        /// </summary>
        public float scaleY
        {
            get
            {
                Matrix m = this.transform.matrix;
                float det = m.a * m.d - m.b * m.c;
                return (float)((det < 0.0f) ?
                    (- global::System.Math.Sqrt(m.c * m.c + m.d * m.d)) :
                    (global::System.Math.Sqrt(m.c * m.c + m.d * m.d)));
            }
            
            set
            {
                Matrix m = this.transform.matrix;
                float det = m.a * m.d - m.b * m.c;
                float sy = (float)((det < 0.0f) ?
                    (- global::System.Math.Sqrt(m.c * m.c + m.d * m.d)) :
                    (global::System.Math.Sqrt(m.c * m.c + m.d * m.d)));

                if (sy != 0.0f)
                {
                    m.c = (m.c / sy) * value;
                    m.d = (m.d / sy) * value;
                }
                else
                {
                    m.c = 0.0f;
                    m.d = value;
                }
            }
        }
        
        /// <summary>
        /// Specifies the transform of the display object.
        /// </summary>
        public Flare.Geom.Transform transform { get; set; }
        
        /// <summary>
        /// Indicates whether this display object is visible.
        /// </summary>
        public bool visible { get; set; }

        /// <summary>
        /// Horizontal coordinate of the display object.
        /// </summary>
        public float x
        {
            get { return transform.matrix.tx; }
            set { transform.matrix.tx = value; }
        }
        
        /// <summary>
        /// Vertical coordinate of the display object.
        /// </summary>
        public float y
        {
            get { return transform.matrix.ty; }
            set { transform.matrix.ty = value; }
        }
        
        public DisplayObject()
        {
            this.blendMode = BlendMode.NORMAL;
            this.transform = new Flare.Geom.Transform();
            this.visible = true;
        }

        /// <summary>
        /// Converts from global stage coordinates to the display object's local coordinates.
        /// </summary>
        public Point GlobalToLocal(Point point)
        {
            if (transform.concatenatedMatrix == null)
            {
                throw new global::System.InvalidOperationException(
                    "transform.concatenatedMatrix is null.");
            }

            Matrix m = new Matrix(transform.concatenatedMatrix);
            m.Invert();
            return m.TransformPoint(point);
        }

        /// <summary>
        /// Checks to see if the display object overlaps the point specified by the global
        /// stage coordinates x and y. If shapeFlag is true then the test is done against
        /// the pixels of the display object instead of its bounding box.
        /// </summary>
        public virtual bool HitTestPoint(float x, float y, bool shapeFlag = false)
        {
            return HitTest(x, y, shapeFlag, true);
        }

        /// <summary>
        /// Actual hit test function that is overridden in subclasses which has an option
        /// to ignore testing children.
        /// </summary>
        internal virtual bool HitTest(float x, float y, bool shapeFlag, bool evaluateChildren)
        {
            return false;
        }

        /// <summary>
        /// Return a clone of the DisplayObject with a deep copy of any mutable members.
        /// </summary>
        public override object Clone()
        {
            DisplayObject clone = (DisplayObject)base.Clone();
            
            // Deep copy the transform for the clone
            clone.transform = new Flare.Geom.Transform(this.transform);
            
            // Clear parent since the clone shouldn't have a parent when initially created
            clone.parent = null;
            
            // Use copied m_blendMode, m_name, m_visible, or m_loaderInfo values
            
            return clone;
        }

        /// <summary>
        /// Render the DisplayObject.
        /// </summary>
        internal virtual void Render(RenderContext context)
        {
        }
        
        /// <summary>
        /// Render the DisplayObject as part of a mask using the current material.
        /// </summary>
        internal virtual void RenderAsMask(RenderContext context)
        {
        }

        //--------------------------------------------------------------------------------

        internal class RenderContext
        {
            public Matrix stageTranslationAndScale { get; set; }
            
            private List<DisplayObject> maskStack { get; set; }
            
            private List<DisplayObject> renderedMaskStack { get; set; }
            
            private bool isMasking { get; set; }
            
            private bool isForceClear { get; set; }
            
            public RenderContext()
            {
                this.stageTranslationAndScale = new Matrix();
                this.maskStack = new List<DisplayObject>();
                this.renderedMaskStack = new List<DisplayObject>();
                this.isMasking = false;
                this.isForceClear = true;
            }
            
            public void PushMask(DisplayObject mask)
            {
                this.maskStack.Add(mask);
            }
            
            public void PopMask()
            {
                this.maskStack.RemoveAt(maskStack.Count - 1);
            }
            
            public void Reset()
            {
                this.maskStack.Clear();
                this.renderedMaskStack.Clear();
                this.isMasking = false;
                this.isForceClear = true;
            }
            
            private static Material ms_stencilMaskMaterial = null;
            
            public void SetupMasks()
            {
                if (ms_stencilMaskMaterial == null)
                {
                    Shader s = Shader.Find("Flare/StencilMask");
                    if (s == null)
                    {
                        Log.Error(Subsystem.Playback,
                                  "Could not find 'Flare/StencilMask' shader. " +
                                  "Please ensure it is in a Resource folder.");
                    }
                    else
                    {
                        ms_stencilMaskMaterial = new Material(s);
                    }
                }
                
                if (this.maskStack.Count == 0)
                {
                    if (this.isMasking || this.isForceClear)
                    {
                        GL.Clear(true, false, Color.black, 1.0f);
                        this.isMasking = false;
                        this.isForceClear = false;
                    }
                }
                else
                {
                    // See if the maskStack has already been rendered or not
                    bool dirty = true;
                    if (this.renderedMaskStack.Count == this.maskStack.Count)
                    {
                        dirty = false;
                        for (int i = 0; i < this.renderedMaskStack.Count; ++i)
                        {
                            if (this.renderedMaskStack[i] != this.maskStack[i])
                            {
                                dirty = true;
                                break;
                            }
                        }
                    }
                    
                    if (dirty || this.isForceClear)
                    {
                        GL.Clear(true, false, Color.black, -1.0f);

                        if ((SystemInfo.supportsStencil > 0) && (this.maskStack.Count > 1))
                        {
                            int last = this.maskStack.Count - 1;

                            // TODO bwmott 2014-06-11: Using the stencil buffer, we can remove
                            // the nesting limit; however, it would run a little slower.
                            // Consider implementing if it becomes an issue.

                            // Unity Pro has stencil buffer support so use it for masking. The
                            // shader is limited to at most 8 nested masks.
                            if (last > 7)
                            {
                                Log.Warning(Subsystem.Playback,
                                    "Masks nested at a depth greater than eight is not supported!");
                                last = 7;
                            }

                            for (int i = 0; i <= last; ++i)
                            {
                                ms_stencilMaskMaterial.SetFloat("_OffsetZ",
                                    (i == last) ? 0.5f : -0.5f);
                                ms_stencilMaskMaterial.SetPass(i + 1);
                                this.maskStack[i].RenderAsMask(this);
                            }
                        }
                        else
                        {
                            // Free version of Unity does not have stencil buffer support so
                            // nested masks cannot be used
                            if (this.maskStack.Count > 1)
                            {
                                Log.Warning(Subsystem.Playback, "Nested masks are not supported " +
                                    "using the free version of Unity!");
                            }

                            ms_stencilMaskMaterial.SetFloat("_OffsetZ", 0.5f);
                            ms_stencilMaskMaterial.SetPass(0);
                            this.maskStack[0].RenderAsMask(this);
                        }
                        
                        this.renderedMaskStack.Clear();
                        this.renderedMaskStack.AddRange(this.maskStack);
                        
                        this.isForceClear = false;
                    }
                    
                    this.isMasking = true;
                }
            }
        }
    }
}
